"""
Decorator analysis for Python code.

Analyzes Python decorators and their effects on function behavior,
including common patterns like property, staticmethod, classmethod, etc.
"""

import ast
from typing import Dict, List, Any, Optional, Set
from collections import defaultdict

from ...core.error_handler import AnalysisError, log_info


class DecoratorAnalyzer:
    """Analyzes Python decorators and their effects."""
    
    def __init__(self):
        """Initialize the decorator analyzer."""
        self.known_decorators = self._initialize_known_decorators()
        self.decorator_effects = {}
    
    def _initialize_known_decorators(self) -> Dict[str, Dict[str, Any]]:
        """Initialize database of known decorators and their effects."""
        return {
            # Built-in decorators
            'property': {
                'type': 'property',
                'effect': 'transforms method into property getter',
                'changes_signature': True,
                'changes_behavior': True,
                'category': 'builtin'
            },
            'staticmethod': {
                'type': 'method_modifier',
                'effect': 'makes method static (no self parameter)',
                'changes_signature': True,
                'changes_behavior': True,
                'category': 'builtin'
            },
            'classmethod': {
                'type': 'method_modifier',
                'effect': 'makes method a class method (cls parameter)',
                'changes_signature': True,
                'changes_behavior': True,
                'category': 'builtin'
            },
            'abstractmethod': {
                'type': 'abstract',
                'effect': 'marks method as abstract',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'abc',
                'module': 'abc'
            },
            
            # Common third-party decorators
            'lru_cache': {
                'type': 'caching',
                'effect': 'adds LRU caching to function',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'functools',
                'module': 'functools'
            },
            'cache': {
                'type': 'caching',
                'effect': 'adds caching to function',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'functools',
                'module': 'functools'
            },
            'wraps': {
                'type': 'wrapper',
                'effect': 'preserves function metadata in wrapper',
                'changes_signature': False,
                'changes_behavior': False,
                'category': 'functools',
                'module': 'functools'
            },
            
            # Web framework decorators
            'app.route': {
                'type': 'routing',
                'effect': 'registers function as HTTP route handler',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'flask'
            },
            'api.route': {
                'type': 'routing',
                'effect': 'registers function as API route handler',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'api'
            },
            
            # Testing decorators
            'pytest.fixture': {
                'type': 'testing',
                'effect': 'marks function as pytest fixture',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'pytest'
            },
            'mock.patch': {
                'type': 'testing',
                'effect': 'patches object during test execution',
                'changes_signature': True,
                'changes_behavior': True,
                'category': 'unittest'
            },
            
            # Async decorators
            'asyncio.coroutine': {
                'type': 'async',
                'effect': 'marks function as coroutine',
                'changes_signature': False,
                'changes_behavior': True,
                'category': 'asyncio'
            }
        }
    
    def analyze_decorators(self, ast_tree: ast.AST) -> Dict[str, Any]:
        """Analyze all decorators in the AST."""
        decorators_info = {
            'decorators': [],
            'decorator_usage': defaultdict(int),
            'decorator_patterns': [],
            'affected_functions': [],
            'decorator_chains': [],
            'statistics': {
                'total_decorators': 0,
                'unique_decorators': 0,
                'builtin_decorators': 0,
                'custom_decorators': 0,
                'property_decorators': 0,
                'method_modifiers': 0
            }
        }
        
        visitor = DecoratorVisitor(self.known_decorators)
        visitor.visit(ast_tree)
        
        decorators_info['decorators'] = visitor.decorators
        decorators_info['affected_functions'] = visitor.affected_functions
        decorators_info['decorator_chains'] = visitor.decorator_chains
        
        # Calculate statistics
        for decorator in visitor.decorators:
            decorator_name = decorator['name']
            decorators_info['decorator_usage'][decorator_name] += 1
            
            if decorator.get('is_known'):
                decorators_info['statistics']['builtin_decorators'] += 1
                decorator_type = decorator.get('info', {}).get('type', '')
                if decorator_type == 'property':
                    decorators_info['statistics']['property_decorators'] += 1
                elif decorator_type == 'method_modifier':
                    decorators_info['statistics']['method_modifiers'] += 1
            else:
                decorators_info['statistics']['custom_decorators'] += 1
        
        decorators_info['statistics']['total_decorators'] = len(visitor.decorators)
        decorators_info['statistics']['unique_decorators'] = len(decorators_info['decorator_usage'])
        
        # Analyze patterns
        decorators_info['decorator_patterns'] = self._analyze_patterns(visitor.decorators)
        
        return decorators_info
    
    def _analyze_patterns(self, decorators: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Analyze common decorator patterns."""
        patterns = []
        
        # Group decorators by target
        by_target = defaultdict(list)
        for decorator in decorators:
            target = decorator['target']
            by_target[target].append(decorator)
        
        # Analyze patterns for each target
        for target, target_decorators in by_target.items():
            if len(target_decorators) > 1:
                # Multiple decorators on same target
                decorator_names = [d['name'] for d in target_decorators]
                patterns.append({
                    'type': 'decorator_chain',
                    'target': target,
                    'decorators': decorator_names,
                    'count': len(target_decorators),
                    'pattern_description': f"Function {target} has {len(target_decorators)} decorators: {', '.join(decorator_names)}"
                })
            
            # Check for specific patterns
            decorator_names = [d['name'] for d in target_decorators]
            
            # Property pattern
            if 'property' in decorator_names:
                patterns.append({
                    'type': 'property_pattern',
                    'target': target,
                    'description': f"Property definition for {target}"
                })
            
            # Getter/Setter pattern
            if any(name.endswith('.setter') for name in decorator_names):
                patterns.append({
                    'type': 'property_setter_pattern',
                    'target': target,
                    'description': f"Property setter for {target}"
                })
            
            # Caching pattern
            if any(name in ['lru_cache', 'cache'] for name in decorator_names):
                patterns.append({
                    'type': 'caching_pattern',
                    'target': target,
                    'description': f"Cached function {target}"
                })
            
            # Web route pattern
            if any('route' in name for name in decorator_names):
                patterns.append({
                    'type': 'web_route_pattern',
                    'target': target,
                    'description': f"Web route handler {target}"
                })
        
        return patterns
    
    def analyze_decorator_effects(self, function_info: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze the effects of decorators on a specific function."""
        effects = {
            'signature_changes': [],
            'behavior_changes': [],
            'call_pattern_changes': [],
            'performance_effects': [],
            'side_effects': []
        }
        
        decorators = function_info.get('decorators', [])
        
        for decorator_name in decorators:
            # Clean decorator name (remove arguments)
            clean_name = self._clean_decorator_name(decorator_name)
            
            if clean_name in self.known_decorators:
                decorator_info = self.known_decorators[clean_name]
                
                if decorator_info.get('changes_signature'):
                    effects['signature_changes'].append({
                        'decorator': decorator_name,
                        'effect': decorator_info.get('effect', ''),
                        'type': decorator_info.get('type', '')
                    })
                
                if decorator_info.get('changes_behavior'):
                    effects['behavior_changes'].append({
                        'decorator': decorator_name,
                        'effect': decorator_info.get('effect', ''),
                        'type': decorator_info.get('type', '')
                    })
                
                # Specific effect analysis
                decorator_type = decorator_info.get('type', '')
                
                if decorator_type == 'caching':
                    effects['performance_effects'].append({
                        'decorator': decorator_name,
                        'effect': 'May improve performance through caching',
                        'impact': 'positive'
                    })
                
                elif decorator_type == 'property':
                    effects['call_pattern_changes'].append({
                        'decorator': decorator_name,
                        'effect': 'Changes call pattern from method() to attribute access',
                        'new_pattern': 'attribute_access'
                    })
                
                elif decorator_type == 'async':
                    effects['call_pattern_changes'].append({
                        'decorator': decorator_name,
                        'effect': 'Requires await keyword for function calls',
                        'new_pattern': 'async_call'
                    })
        
        return effects
    
    def _clean_decorator_name(self, decorator_name: str) -> str:
        """Clean decorator name by removing arguments and prefixes."""
        # Remove function call syntax
        if '(' in decorator_name:
            decorator_name = decorator_name.split('(')[0]
        
        # Handle attribute access (e.g., app.route -> route)
        if '.' in decorator_name:
            parts = decorator_name.split('.')
            # Try full name first, then just the last part
            if decorator_name in self.known_decorators:
                return decorator_name
            else:
                return parts[-1]
        
        return decorator_name
    
    def get_decorator_dependencies(self, decorators: List[str]) -> List[Dict[str, Any]]:
        """Get dependencies (imports) required for decorators."""
        dependencies = []
        
        for decorator_name in decorators:
            clean_name = self._clean_decorator_name(decorator_name)
            
            if clean_name in self.known_decorators:
                decorator_info = self.known_decorators[clean_name]
                module = decorator_info.get('module')
                
                if module:
                    dependencies.append({
                        'decorator': decorator_name,
                        'required_module': module,
                        'import_statement': f"from {module} import {clean_name}"
                    })
        
        return dependencies


class DecoratorVisitor(ast.NodeVisitor):
    """AST visitor for collecting decorator information."""
    
    def __init__(self, known_decorators: Dict[str, Dict[str, Any]]):
        """Initialize the decorator visitor."""
        self.known_decorators = known_decorators
        self.decorators = []
        self.affected_functions = []
        self.decorator_chains = []
        self.current_class = None
    
    def visit_ClassDef(self, node: ast.ClassDef):
        """Visit class definition."""
        old_class = self.current_class
        self.current_class = node.name
        
        # Process class decorators
        for decorator in node.decorator_list:
            self._process_decorator(decorator, node.name, 'class')
        
        self.generic_visit(node)
        self.current_class = old_class
    
    def visit_FunctionDef(self, node: ast.FunctionDef):
        """Visit function definition."""
        self._process_function_decorators(node)
        self.generic_visit(node)
    
    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
        """Visit async function definition."""
        self._process_function_decorators(node)
        self.generic_visit(node)
    
    def _process_function_decorators(self, node):
        """Process decorators for a function."""
        function_name = node.name
        target_name = f"{self.current_class}.{function_name}" if self.current_class else function_name
        
        if node.decorator_list:
            # Track affected function
            self.affected_functions.append({
                'name': function_name,
                'qualified_name': target_name,
                'class': self.current_class,
                'decorator_count': len(node.decorator_list),
                'decorators': [self._ast_to_string(dec) for dec in node.decorator_list],
                'line': node.lineno
            })
            
            # Track decorator chain if multiple decorators
            if len(node.decorator_list) > 1:
                chain = [self._ast_to_string(dec) for dec in node.decorator_list]
                self.decorator_chains.append({
                    'target': target_name,
                    'decorators': chain,
                    'order': 'bottom_to_top',  # Python applies decorators bottom to top
                    'line': node.lineno
                })
        
        # Process individual decorators
        for decorator in node.decorator_list:
            self._process_decorator(decorator, target_name, 'function')
    
    def _process_decorator(self, decorator: ast.AST, target: str, target_type: str):
        """Process a single decorator."""
        decorator_name = self._ast_to_string(decorator)
        
        # Check if it's a known decorator
        clean_name = self._clean_decorator_name(decorator_name)
        is_known = clean_name in self.known_decorators
        
        decorator_info = {
            'name': decorator_name,
            'clean_name': clean_name,
            'target': target,
            'target_type': target_type,
            'line': decorator.lineno,
            'col': decorator.col_offset,
            'is_known': is_known
        }
        
        if is_known:
            decorator_info['info'] = self.known_decorators[clean_name]
        
        # Extract decorator arguments if present
        if isinstance(decorator, ast.Call):
            decorator_info['has_arguments'] = True
            decorator_info['arg_count'] = len(decorator.args)
            decorator_info['kwarg_count'] = len(decorator.keywords)
            
            # Try to extract simple argument values
            args = []
            for arg in decorator.args:
                if isinstance(arg, ast.Constant):
                    args.append(arg.value)
                else:
                    args.append(self._ast_to_string(arg))
            decorator_info['arguments'] = args
        else:
            decorator_info['has_arguments'] = False
        
        self.decorators.append(decorator_info)
    
    def _ast_to_string(self, node: ast.AST) -> str:
        """Convert AST node to string representation."""
        try:
            return ast.unparse(node)
        except:
            # Fallback for older Python versions
            if isinstance(node, ast.Name):
                return node.id
            elif isinstance(node, ast.Attribute):
                return f"{self._ast_to_string(node.value)}.{node.attr}"
            elif isinstance(node, ast.Call):
                func_name = self._ast_to_string(node.func)
                return f"{func_name}(...)"
            else:
                return '<unknown>'
    
    def _clean_decorator_name(self, decorator_name: str) -> str:
        """Clean decorator name by removing arguments and prefixes."""
        if '(' in decorator_name:
            decorator_name = decorator_name.split('(')[0]
        
        if '.' in decorator_name:
            parts = decorator_name.split('.')
            if decorator_name in self.known_decorators:
                return decorator_name
            else:
                return parts[-1]
        
        return decorator_name